//
//  XMLNode.swift
//  C150805_libxml2
//
//  Created by yao_yu on 15/8/5.
//  Copyright © 2015年 yao_yu. All rights reserved.
//

import Foundation

enum XMLNodeType: Int {
    case Element = 1
    case Attribute = 2
    case Text = 3
    case CDataSection = 4
    case EntityRef = 5
    case Entity = 6
    case Pi = 7
    case Comment = 8
    case Document = 9
    case DocumentType = 10
    case DocumentFrag = 11
    case Notation = 12
    case HtmlDocument = 13
    case DTD = 14
    case ElementDecl = 15
    case AttributeDecl = 16
    case EntityDecl = 17
    case NamespaceDecl = 18
    case XIncludeStart = 19
    case XIncludeEnd = 20
    case DocbDocument = 21
}

class XMLNode {
    let xmlNode:xmlNodePtr
    let xmlDocument: XMLDocument
    let type:XMLNodeType
    
    private var _keepTextNodePrevious = false
    var keepTextNode = false
    
    init(xmlNode:xmlNodePtr, xmlDocument:XMLDocument, keepTextNode:Bool = false) {
        self.xmlNode = xmlNode
        self.xmlDocument = xmlDocument
        self.keepTextNode = keepTextNode
        self.type = XMLNodeType(rawValue: Int(xmlNode.memory.type.rawValue))!
    }
    
    // The tag name of this node.
    var tagName: String? { return name }
    
    // The tag name of this node.
    lazy var name: String? = {
        return String.fromXmlChar(self.xmlNode.memory.name)
        }()
    
    private var _children = [XMLNode]()
    private var _childrenHasBeenCalculated = false
    var children: [XMLNode] {
        if _childrenHasBeenCalculated && keepTextNode == _keepTextNodePrevious {
            return _children
        } else {
            _children = [XMLNode]()
            
            for var childNodePointer = xmlNode.memory.children;
                childNodePointer != nil;
                childNodePointer = childNodePointer.memory.next
            {
                if keepTextNode || xmlNodeIsText(childNodePointer) == 0 {
                    let childNode = XMLNode(xmlNode: childNodePointer, xmlDocument: xmlDocument, keepTextNode: keepTextNode)
                    _children.append(childNode)
                }
            }
            _childrenHasBeenCalculated = true
            _keepTextNodePrevious = keepTextNode
            return _children
        }
    }
    
    var firstChild:XMLNode? {
        return children.first
    }
    
    var lastChild:XMLNode? {
        return children.last
    }
    
    var hasChildren:Bool {
        return children.count > 0
    }
    
    lazy var parent: XMLNode? = {
        if self.xmlNode.memory.parent == nil { return nil }
        return XMLNode(xmlNode: self.xmlNode.memory.parent, xmlDocument: self.xmlDocument, keepTextNode: self.keepTextNode)
        }()
    
    var nextSibling: XMLNode? {
        var next = xmlNode.memory.next
        if next == nil { return nil }
        if keepTextNode {
            return XMLNode(xmlNode: next, xmlDocument: xmlDocument, keepTextNode: keepTextNode)
        } else {
            while xmlNodeIsText(next) != 0 {
                next = next.memory.next
                if next == nil { return nil }
            }
            return XMLNode(xmlNode: next, xmlDocument: xmlDocument, keepTextNode: keepTextNode)
        }
    }
    
    var previousSibling: XMLNode? {
        var prev = xmlNode.memory.prev
        if prev == nil { return nil }
        if keepTextNode {
            return XMLNode(xmlNode: prev, xmlDocument: xmlDocument, keepTextNode: keepTextNode)
        } else {
            while xmlNodeIsText(prev) != 0 {
                prev = prev.memory.prev
                if prev == nil { return nil }
            }
            return XMLNode(xmlNode: prev, xmlDocument: xmlDocument, keepTextNode: keepTextNode)
        }
    }
    
    lazy var rawContent: String? = {
        let contentChars = xmlNodeGetContent(self.xmlNode)
        if contentChars == nil { return nil }
        let contentString = String.fromXmlChar(contentChars)
        free(contentChars)
        return contentString
        }()
    
    lazy var content: String? = {
        return self.rawContent?.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())

        }()
    
    lazy var rawValue: String? = {
        let valueChars = xmlNodeListGetString(self.xmlDocument.xmlDoc, self.xmlNode.memory.children, 1)
        if valueChars == nil { return nil }
        let valueString = String.fromXmlChar(valueChars)
        free(valueChars)
        return valueString
        }()
    
    lazy var value: String? = {
        return self.rawValue?.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
        }()
    
    subscript(key: String) -> String? {
        return attributes[key]
    }
    
    lazy var attributes: [String: String] = {
        var result = [String: String]()
        for var attribute: xmlAttrPtr = self.xmlNode.memory.properties;
            attribute != nil;
            attribute = attribute.memory.next
        {
            let key = String.fromXmlChar(attribute.memory.name)
            assert(key != nil, "key doesn't exist")
            let valueChars = xmlNodeGetContent(attribute.memory.children)
            var value: String? = ""
            if valueChars != nil {
                value = String.fromXmlChar(valueChars)
                assert(value != nil, "value doesn't exist")
            }
            free(valueChars)
            
            result[key!] = value!
        }
        return result
        }()
    
    func xPath(xPath: String) -> [XMLNode] {
        let xPathContext = xmlXPathNewContext(self.xmlDocument.xmlDoc)
        if xPathContext == nil {
            return []
        }
        
        xPathContext.memory.node = self.xmlNode
        
        let xPathObject = xmlXPathEvalExpression(UnsafePointer<xmlChar>(xPath.cStringUsingEncoding(NSUTF8StringEncoding)!), xPathContext)
        xmlXPathFreeContext(xPathContext)
        if xPathObject == nil {
            return []
        }
        
        let nodeSet = xPathObject.memory.nodesetval
        if nodeSet == nil || nodeSet.memory.nodeNr == 0 || nodeSet.memory.nodeTab == nil {
            xmlXPathFreeObject(xPathObject)
            return []
        }
        
        var resultNodes = [XMLNode]()
        for i in 0 ..< Int(nodeSet.memory.nodeNr) {
            let xmlNode = XMLNode(xmlNode: nodeSet.memory.nodeTab[i], xmlDocument: self.xmlDocument, keepTextNode: keepTextNode)
            resultNodes.append(xmlNode)
        }
        
        xmlXPathFreeObject(xPathObject)
        
        return resultNodes
    }
    
    func firstChildWithName(name: String) -> XMLNode? {
        for node in children {
            if node.name == name {
                return node
            }
        }
        return nil
    }
    
    func childrenWithName(name: String) -> [XMLNode] {
        return children.filter { $0.name == name }
    }
    
    func firstChildWithAttributeName(attributeName: String, attributeValue: String) -> XMLNode? {
        for node in children {
            if node[attributeName] == attributeValue {
                return node
            }
        }
        return nil
    }
    
    func childrenWithAttributeName(attributeName: String, attributeValue: String) -> [XMLNode] {
        return children.filter { $0.attributes[attributeName] == attributeValue }
    }
    
    func firstDescendantWithName(name: String) -> XMLNode? {
        return firstDescendantWithName(name, node: self)
    }
    
    func firstDescendantWithName(name: String, node: XMLNode) -> XMLNode? {
        if !node.hasChildren {
            return nil
        }
        
        for child in node.children {
            if child.name == name {
                return child
            }
            if let nodeFound = firstDescendantWithName(name, node: child) {
                return nodeFound
            }
        }
        return nil
    }
    
    func descendantsWithName(name: String) -> [XMLNode] {
        return descendantsWithName(name, node: self)
    }
    

    func descendantsWithName(name: String, node: XMLNode) -> [XMLNode] {
        if !node.hasChildren {
            return []
        }
        
        var results = [XMLNode]()
        for child in node.children {
            if child.name == name {
                results.append(child)
            }
            results.extend(descendantsWithName(name, node: child))
        }
        return results
    }
    
    func firstDescendantWithAttributeName(attributeName: String, attributeValue: String) -> XMLNode? {
        return firstDescendantWithAttributeName(attributeName, attributeValue: attributeValue, node: self)
    }
    
    private func firstDescendantWithAttributeName(attributeName: String, attributeValue: String, node: XMLNode) -> XMLNode? {
        if !node.hasChildren {
            return nil
        }
        
        for child in node.children {
            if child[attributeName] == attributeValue {
                return child
            }
            if let nodeFound = firstDescendantWithAttributeName(attributeName, attributeValue: attributeValue, node: child) {
                return nodeFound
            }
        }
        return nil
    }
    
    func descendantsWithAttributeName(attributeName: String, attributeValue: String) -> [XMLNode] {
        return descendantsWithAttributeName(attributeName, attributeValue: attributeValue, node: self)
    }
    
    private func descendantsWithAttributeName(attributeName: String, attributeValue: String, node: XMLNode) -> [XMLNode] {
        if !node.hasChildren {
            return []
        }
        
        var results = [XMLNode]()
        for child in node.children {
            if child[attributeName] == attributeValue {
                results.append(child)
            }
            results.extend(descendantsWithAttributeName(attributeName, attributeValue: attributeValue, node: child))
        }
        return results
    }

}

extension XMLNode: Equatable { }
func ==(lhs: XMLNode, rhs: XMLNode) -> Bool {
    if (lhs.xmlNode != nil) && (rhs.xmlNode != nil) && (lhs.xmlDocument == rhs.xmlDocument) {
        return xmlXPathCmpNodes(lhs.xmlNode, rhs.xmlNode) == 0
    }
    return false
}
